﻿//===================================================================================
// Copyright (c) Ryan Garrett.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===================================================================================

//WRITE: Basically all models have to have default constructors. They have to push everything to the OnInitialized method
//WRITE: Remember to write about how the SetItem works and how you can use Swap instead
//WRITE: Remember to talk about the difference in terminology between 'removed' and 'purged'
//WRITE: Remember to write about when the CollectionChanged event is fired. Added, Committed->Removed, Removed->Committed
//WRITE: Remember to document that when iterating over a ModelCollection, you cannot remove items from it.  You can commit them though. To remove, first do a ToList() on the collection.
//      This also applies to the UndoRemove/Commit operations and iterating the GetRemovedModels() method.
//WRITE: Be sure to write about how the deserialization process does not go through the typical event cycle. All events are completely circumvented
//WRITE: Remember to write about how order is not guaranteed unless the collection is sorted
//FUTURE: Need to create an OnDeserialized event in the Creator class
//TODO: Test Async add methods

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Caffeine.Core.Metadata;
using System.Threading;

namespace Caffeine.Core
{
    /// <summary>
    /// Delegate for validating model collections
    /// </summary>
    /// <typeparam name="T">Type of model contained in the collection</typeparam>
    /// <param name="collection">Collection to validate</param>
    /// <param name="validationErrors">Errors found during validation</param>
    /// <param name="isValid">Validation result</param>
    public delegate void CollectionValidationMethod<T>
    (
        ModelCollection<T> collection,
        ref IEnumerable<ValidationError> validationErrors,
        ref Boolean isValid
    ) where T : ModelBase, new();

    /// <summary>
    /// A collection for models
    /// </summary>
    /// <typeparam name="T">Type of model this collection will contain</typeparam>
    public sealed class ModelCollection<T> : ModelCollectionBase, IList<T>, INotifyCollectionChanged 
        where T : ModelBase, new()
    {
        #region State

        private Int32 _addedModelCount;
        private Int32 _removedModelCount;
        private Int32 _committedModelCount;
        private Boolean _containsAddedModels;
        private Boolean _containsRemovedModels;
        private Boolean _containsCommittedModels;
        private NotifyCollectionChangedEventHandler _collectionChanged;
        private readonly Boolean _supportsPropertyChangeTracking;
        private readonly List<T> _internalList;
        private readonly List<T> _internalRemovedModels;

        #endregion

        #region Constructors

        internal ModelCollection(ModelBase parent, String name, Nullable<ModelPropertyBehaviors> behavior, Boolean isWrapped) : 
            base(parent, name, behavior, isWrapped)
        {
            _internalList = new List<T>();
            _internalRemovedModels = new List<T>();
            _supportsPropertyChangeTracking = this.CheckSupportsPropertyChangeCounting();
        }

        #endregion

        #region Public

        /// <summary>
        /// Raised whenever this collection is changed
        /// </summary>
        public event NotifyCollectionChangedEventHandler CollectionChanged
        {
            add
            {
                _collectionChanged += value;
            }
            remove
            {
                _collectionChanged -= value;
            }
        }

        /// <summary>
        /// Gets whether or not this collection is readonly
        /// </summary>
        public override Boolean IsReadOnly
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        /// Gets an enumeration of all models added to this collection. This property supports INPC.
        /// </summary>
        public IEnumerable<T> AddedModels
        {
            get
            {
                return InternalList.Where(CheckIsAddedModel);
            }
        }

        /// <summary>
        /// Gets an enumeration of all models removed from this collection. This property supports INPC.
        /// </summary>
        public IEnumerable<T> RemovedModels
        {
            get
            {
                return InternalRemovedModels;
            }
        }

        /// <summary>
        /// Gets an enumeration of all models committed to this collection. This property supports INPC.
        /// </summary>
        public IEnumerable<T> CommittedModels
        {
            get
            {
                return InternalList.Where(CheckIsCommittedModel);
            }
        }

        /// <summary>
        /// Gets whether or not there are any models added to this collection. This property supports INPC.
        /// </summary>
        public override Boolean ContainsAddedModels
        {
            get
            {
                return _containsAddedModels;
            }
            protected set
            {
                if (_containsAddedModels != value)
                {
                    _containsAddedModels = value;
                    RaiseNotifyPropertyChanged("ContainsAddedModels");

                    AssignIsDirty();
                }
            }
        }

        /// <summary>
        /// Gets whether or not there are any models removed from this collection. This property supports INPC.
        /// </summary>
        public override Boolean ContainsRemovedModels
        {
            get
            {
                return _containsRemovedModels;
            }
            protected set
            {
                if (_containsRemovedModels != value)
                {
                    _containsRemovedModels = value;
                    RaiseNotifyPropertyChanged("ContainsRemovedModels");

                    AssignIsDirty();
                }
            }
        }

        /// <summary>
        /// Gets whether or not there are any models committed to this collection. This property supports INPC.
        /// </summary>
        public override Boolean ContainsCommittedModels
        {
            get
            {
                return _containsCommittedModels;
            }
            protected set
            {
                if (_containsCommittedModels != value)
                {
                    _containsCommittedModels = value;
                    RaiseNotifyPropertyChanged("ContainsCommittedModels");
                }
            }
        }

        /// <summary>
        /// Gets the number of added or committed models. This property supports INPC.
        /// </summary>
        public override Int32 Count
        {
            get
            {
                return InternalList.Count;
            }
        }

        /// <summary>
        /// Gets the number of models added to this collection. This property supports INPC.
        /// </summary>
        public override Int32 AddedModelCount
        {
            get
            {
                return _addedModelCount;
            }
            protected set
            {
                if (_addedModelCount != value)
                {
                    _addedModelCount = value;
                    RaiseNotifyPropertyChanged("AddedModelCount");
                    RaiseNotifyPropertyChanged("AddedModels");
                    ContainsAddedModels = value > 0;
                }
            }
        }

        /// <summary>
        /// Gets the number of models removed from this collection. This property supports INPC.
        /// </summary>
        public override Int32 RemovedModelCount
        {
            get
            {
                return _removedModelCount;
            }
            protected set
            {
                if (_removedModelCount != value)
                {
                    _removedModelCount = value;
                    RaiseNotifyPropertyChanged("RemovedModelCount");
                    RaiseNotifyPropertyChanged("RemovedModels");
                    ContainsRemovedModels = value > 0;
                }
            }
        }

        /// <summary>
        /// Gets the number of models committed to this collection. This property supports INPC.
        /// </summary>
        public override Int32 CommittedModelCount
        {
            get
            {
                return _committedModelCount;
            }
            protected set
            {
                if (_committedModelCount != value)
                {
                    _committedModelCount = value;
                    RaiseNotifyPropertyChanged("CommittedModelCount");
                    RaiseNotifyPropertyChanged("CommittedModels");
                    ContainsCommittedModels = value > 0;
                }
            }
        }

        /// <summary>
        /// Gets the type of model contained by this collection
        /// </summary>
        public override Type PropertyType
        {
            get
            {
                return typeof(T);
            }
        }

        /// <summary>
        /// Gets or sets the validation method of this collection
        /// </summary>
        public CollectionValidationMethod<T> ValidationMethod
        {
            get;
            set;
        }

        /// <summary>
        /// Commit this collection. Added models will become committed models. Removed models will
        /// be purged from the collection.
        /// </summary>
        public override void Commit()
        {
            if (ContainsAddedModels)
            {
                for (Int32 index = 0; index < Count; index++)
                {
                    T model = InternalList[index];

                    if (model.IsAddedToCollection)
                    {
                        CommitInternal(model);
                    }
                }
            }

            if (ContainsRemovedModels)
            {
                while (InternalRemovedModels.Count > 0)
                {
                    CommitInternal(InternalRemovedModels[0]);
                }
            }
        }

        /// <summary>
        /// Commit a model to this collection
        /// </summary>
        /// <param name="item">Model to commit</param>
        public void Commit(T item)
        {
            CommitInternal(item);
        }

        /// <summary>
        /// Add a model to this collection
        /// </summary>
        /// <param name="item">Model to add</param>
        public void Add(T item)
        {
            Add(item, false);
        }

        /// <summary>
        /// Add a model to this collection on the UI thread. This method uses the SynchronizationContext.Current singleton
        /// to post an add operation for this collection
        /// </summary>
        /// <param name="item">Model to add</param>
        public void AddAsync(T item)
        {
            AddAsync(item, false);
        }

        /// <summary>
        /// Add a model to this collection
        /// </summary>
        /// <param name="item">Model to add</param>
        /// <param name="addCommitted">
        /// Passing true will cause the model to be immediately considered committed to this collection. False will cause
        /// it to be considered added to this collection.
        /// </param>
        public void Add(T item, Boolean addCommitted)
        {
            InsertInternal(Count, item, false, addCommitted, true);
        }

        /// <summary>
        /// Add a model to this collection on the UI thread. This method uses the SynchronizationContext.Current singleton
        /// to post an add operation for this collection
        /// </summary>
        /// <param name="item">Model to add</param>
        /// <param name="addCommitted">
        /// Passing true will cause the model to be immediately considered committed to this collection. False will cause
        /// it to be considered added to this collection.
        /// </param>
        public void AddAsync(T item, Boolean addCommitted)
        {
            SynchronizationContext.Current.Post(OnAddAsync, new Object[] { item, addCommitted });
        }

        /// <summary>
        /// Adds an array of models to this collection
        /// </summary>
        /// <param name="items">Array of models</param>
        public void Add(T[] items)
        {
            Add(items, false);
        }

        /// <summary>
        /// Adds an array of models to this collection
        /// </summary>
        /// <param name="items">Array of models</param>
        /// <param name="addCommitted">
        /// Passing true will cause the models to be immediately considered committed to this collection. False will cause
        /// them to be considered added to this collection.
        /// </param>
        public void Add(T[] items, Boolean addCommitted)
        {
            if (items == null)
            {
                throw new ArgumentNullException("items");
            }

            if (items.Length > 0)
            {
                for (Int32 index = 0; index < items.Length; index++)
                {
                    InsertInternal(Count, items[index], false, addCommitted, false);
                }

                RaiseReset();
                Validate();
            }
        }

        /// <summary>
        /// Inserts a model into this collection
        /// </summary>
        /// <param name="index">Index to insert model</param>
        /// <param name="item">Model to insert</param>
        public void Insert(Int32 index, T item)
        {
            Insert(index, item, false);
        }

        /// <summary>
        /// Inserts a model into this collection
        /// </summary>
        /// <param name="index">Index to insert model</param>
        /// <param name="item">Model to insert</param>
        /// <param name="insertCommitted">
        /// Passing true will cause this model to be immediately considered committed to this collection. False will cause
        /// it to be considered added to this collection.
        /// </param>
        public void Insert(Int32 index, T item, Boolean insertCommitted)
        {
            InsertInternal(index, item, false, insertCommitted, true);
        }

        /// <summary>
        /// Removes a model at the index specified
        /// </summary>
        /// <param name="index">Index of model to remove</param>
        public void RemoveAt(Int32 index)
        {
            RemoveAt(index, false);
        }

        /// <summary>
        /// Removes a model at the index specified
        /// </summary>
        /// <param name="index">Index of model to remove</param>
        /// <param name="removeCommitted">
        /// Passing true will cause the model to be permanently removed from this collection. Meaning it will not show up in the
        /// RemovedModels enumeration. False will cause it to remain in the RemovedModels enumeration until it is committed.
        /// </param>
        public void RemoveAt(Int32 index, Boolean removeCommitted)
        {
            RemoveInternal(this[index], index, removeCommitted, true);
        }

        /// <summary>
        /// Remove a model from this collection
        /// </summary>
        /// <param name="item">Model to remove</param>
        /// <returns>True if the model was removed, otherwise false</returns>
        public Boolean Remove(T item)
        {
            return Remove(item, false);
        }

        /// <summary>
        /// Remove a model from this collection
        /// </summary>
        /// <param name="item">Model to remove</param>
        /// <param name="removeCommitted">
        /// Passing true will cause the model to be permanently removed from this collection. Meaning it will not show up in the
        /// RemovedModels enumeration. False will cause it to remain in the RemovedModels enumeration until it is committed.
        /// </param>
        /// <returns>True if the model was removed, otherwise false</returns>
        public Boolean Remove(T item, Boolean removeCommitted)
        {
            Int32 index = InternalList.IndexOf(item);
            Boolean canBeRemoved = index >= 0;

            if (canBeRemoved)
            {
                RemoveInternal(item, index, removeCommitted, true);
            }

            return canBeRemoved;
        }

        /// <summary>
        /// Restores a removed model back to the CommittedModels enumeration 
        /// </summary>
        /// <param name="item">Model to restore</param>
        public void UndoRemove(T item)
        {
            if (item == null)
            {
                throw new ArgumentNullException("item");
            }

            if (!item.IsRemovedFromCollection || item.ParentProperty != this)
            {
                throw new ArgumentException("Model is not considered removed from this collection", "item");
            }

            InsertInternal(Count, item, true, false, true);
        }

        /// <summary>
        /// Clears the collection. All added models will be purged from the collection. All committed models will move to the 
        /// RemovedModels enumeration. Nothing will happen to models already in the RemovedModels enumeration.
        /// </summary>
        public void Clear()
        {
            Clear(false);
        }

        /// <summary>
        /// Clears the collection.
        /// </summary>
        /// <param name="clearCommitted">
        /// If true is passed in, all models in all enumerations (AddedModels, CommittedModels, RemovedModels) will
        /// be completely purged from this collection. If false is passed in, this call is no different than the call to the parameter-less Clear() method.
        /// </param>
        public void Clear(Boolean clearCommitted)
        {
            if (clearCommitted && ContainsRemovedModels)
            {
                while (ContainsRemovedModels)
                {
                    Commit(InternalRemovedModels[0]);
                }
            }

            if (Count > 0)
            {
                while (Count > 0)
                {
                    RemoveInternal(this[0], 0, clearCommitted, false);
                }

                RaiseReset();
                Validate();
            }
        }

        /// <summary>
        /// Swaps the indices of two models in this collection
        /// </summary>
        /// <param name="x">Model added or committed to this collection</param>
        /// <param name="y">Model added or committed to this collection</param>
        public void Swap(T x, T y)
        {
            if (x == null)
            {
                throw new ArgumentNullException("x");
            }

            if (y == null)
            {
                throw new ArgumentNullException("y");
            }

            if ((!x.IsAddedToCollection && !x.IsCommittedToCollection) || x.ParentProperty != this)
            {
                throw new ArgumentException("Is not considered committed or added to this collection", "x");
            }

            if ((!y.IsAddedToCollection && !y.IsCommittedToCollection) || y.ParentProperty != this)
            {
                throw new ArgumentException("Is not considered committed or added to this collection", "y");
            }

            Swap(x, IndexOf(x), y, IndexOf(y));
        }

        /// <summary>
        /// Swaps the indices of two models in this collection
        /// </summary>
        /// <param name="x">Index of a model in this collection</param>
        /// <param name="y">Index of a model in this collection</param>
        public void Swap(Int32 x, Int32 y)
        {
            Swap(this[x], x, this[y], y);
        }

        /// <summary>
        /// Gets the index of a model that is added or committed to this collection
        /// </summary>
        /// <param name="item">Model that is added or committed to this collection</param>
        /// <returns>Index of a model that is added or committed to this collection</returns>
        public Int32 IndexOf(T item)
        {
            return InternalList.IndexOf(item);
        }

        /// <summary>
        /// Gets or sets a model at the index specified. Using the setter of this indexer is the functional equivalent of 
        /// calling Insert(index, value) and immediately calling RemoveAt(index + 1).
        /// </summary>
        /// <param name="index">Index to retrieve or set model to</param>
        /// <returns>Model at the index specified</returns>
        public T this[Int32 index]
        {
            get
            {
                return InternalList[index];
            }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException("value");
                }

                if (index < 0 || index >= Count)
                {
                    throw new ArgumentOutOfRangeException("index");
                }

                Insert(index, value);
                RemoveAt(index + 1);
            }
        }

        /// <summary>
        /// Gets whether or not a model exists in this collection. The RemovedModels enumeration is not considered in this operation.
        /// </summary>
        /// <param name="item">Model to search for</param>
        /// <returns>True if the model is added or committed to this collection, otherwise false</returns>
        public Boolean Contains(T item)
        {
            return InternalList.Contains(item);
        }

        /// <summary>
        /// Copies the contents of this collection into an array
        /// </summary>
        /// <param name="array">Array to fill</param>
        /// <param name="arrayIndex">Array index of first model to be copied</param>
        public void CopyTo(T[] array, Int32 arrayIndex)
        {
            if (arrayIndex >= Count)
            {
                throw new ArgumentOutOfRangeException("arrayIndex");
            }

            if (array == null)
            {
                throw new ArgumentNullException("array");
            }

            Int32 fillIndex = 0;

            for (Int32 index = arrayIndex; index < Count; index++)
            {
                array[fillIndex++] = this[index];
            }
        }

        /// <summary>
        /// Release any event handlers registered with this collection
        /// </summary>
        public override void ReleaseObservers()
        {
            base.ReleaseObservers();
            _collectionChanged = null;
        }

        /// <summary>
        /// Gets an enumeration of all added or commited models in this collection
        /// </summary>
        /// <returns>An enumeration of all added or commited models in this collection</returns>
        public override IEnumerator<ModelBase> GetEnumerator()
        {
            return InternalList.GetEnumerator();
        }

        /// <summary>
        /// Gets an enumeration of all models added to this collection
        /// </summary>
        /// <returns>An enumeration of all models added to this collection</returns>
        public override IEnumerable<ModelBase> GetAddedModels()
        {
            return InternalList.Where(CheckIsAddedModel);
        }

        /// <summary>
        /// Gets an enumeration of all models committed to this collection
        /// </summary>
        /// <returns>An enumeration of all models committed to this collection</returns>
        public override IEnumerable<ModelBase> GetCommittedModels()
        {
            return InternalList.Where(CheckIsCommittedModel);
        }

        /// <summary>
        /// Gets an enumeration of all models removed from this collection
        /// </summary>
        /// <returns>An enumeration of all models removed from this collection</returns>
        public override IEnumerable<ModelBase> GetRemovedModels()
        {
            return InternalRemovedModels;
        }

        /// <summary>
        /// Assign this collection to its default value. Functionally equivalent to calling Clear().
        /// </summary>
        public override void AssignDefaultValue()
        {
            Clear();
        }

        /// <summary>
        /// Gets an enumeration of all added or commited models in this collection
        /// </summary>
        /// <returns>An enumeration of all added or commited models in this collection</returns>
        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            return InternalList.GetEnumerator();
        }

        #endregion

        #region Internal

        internal override Boolean SupportsPropertyChangeTracking
        {
            get
            {
                return _supportsPropertyChangeTracking;
            }
        }

        internal override ModelCollectionMetadata GetMetadata()
        {
            ModelCollectionMetadata metadata = new ModelCollectionMetadata()
            {
                Name = Name,
                IsDirty = IsDirty,
                IsInvalid = IsInvalid,
                ValidationErrors = GetValidationErrorMetadata()
            };

            ModelMetadata[] models = null;

            if (InternalList.Count > 0)
            {
                models = new ModelMetadata[InternalList.Count];

                for (Int32 index = 0; index < InternalList.Count; index++)
                {
                    models[index] = InternalList[index].GetMetadata();
                }

                metadata.Children = models;
            }

            if (InternalRemovedModels.Count > 0)
            {
                models = new ModelMetadata[InternalRemovedModels.Count];

                for (Int32 index = 0; index < InternalRemovedModels.Count; index++)
                {
                    models[index] = InternalRemovedModels[index].GetMetadata();
                }

                metadata.RemovedChildren = models;
            }

            return metadata;
        }

        internal override Object GetCollectionChangedForTesting()
        {
            return _collectionChanged;
        }

        internal override Object GetValidationMethodForTesting()
        {
            return ValidationMethod;
        }

        internal override void MergeAdjacentProperty(ModelPropertyBase property)
        {
            ModelCollection<T> sourceCollection = property as ModelCollection<T>;
            HashSet<Guid> validModelUniqueIds = new HashSet<Guid>();

            foreach (T sourceModel in sourceCollection)
            {
                if (sourceModel.UniqueId == null || sourceModel.UniqueId == Guid.Empty)
                {
                    throw new InvalidOperationException("All models in merge must have their UniqueId property assigned");
                }

                T targetModel = this.FirstOrDefault<T>(m => m.UniqueId == sourceModel.UniqueId);

                if (targetModel == null)
                {
                    targetModel = Parent.ParentFactory.CreateModel(sourceModel.GetType()) as T;
                    targetModel.UniqueId = sourceModel.UniqueId;
                    Add(targetModel);
                }

                targetModel.Merge(sourceModel);
                validModelUniqueIds.Add(targetModel.UniqueId.Value);
            }

            List<T> modelsToRemove = new List<T>();

            foreach (T model in this)
            {
                if (model.UniqueId == null || model.UniqueId == Guid.Empty)
                {
                    throw new InvalidOperationException("All models in merge must have their UniqueId property assigned");
                }

                if (!validModelUniqueIds.Contains(model.UniqueId.Value))
                {
                    modelsToRemove.Add(model);
                }
            }

            foreach (T model in modelsToRemove)
            {
                Remove(model);
            }
        }

        #endregion

        #region Protected

        protected override void OnReset()
        {
            Boolean resetOccurred = false;

            while (ContainsRemovedModels)
            {
                resetOccurred = true;
                T model = InternalRemovedModels[0];
                InsertInternal(Count, model, true, false, false);
            }

            for (Int32 index = 0; index < Count; index++)
            {
                T model = this[index];

                if (model.IsAddedToCollection)
                {
                    resetOccurred = true;
                    RemoveInternal(model, index, false, false);
                    index--;
                }
            }

            if (resetOccurred)
            {
                RaiseReset();
                Validate();
            }
        }

        protected override Boolean CheckIsValidationMethodSet()
        {
            return ValidationMethod != null;
        }

        protected override void Validate(ref IEnumerable<ValidationError> validationFailureReasons, ref Boolean isValid)
        {
            validationFailureReasons = null;
            ValidationMethod(this, ref validationFailureReasons, ref isValid);
        }

        protected override void OnApplyCollectionMetadata(ModelCollectionMetadata metadata, IModelFactory factory)
        {
            if (metadata.Children != null)
            {
                for (Int32 index = 0; index < metadata.Children.Length; index++)
                {
                    ModelMetadata modelMetadata = metadata.Children[index];

                    if (modelMetadata.IsAddedToCollection)
                    {
                        _addedModelCount++;
                    }
                    else
                    {
                        _committedModelCount++;
                    }

                    _internalList.Add(factory.CreateModel<T>(metadata.Children[index], this));
                }

                _containsAddedModels = _addedModelCount > 0;
                _containsCommittedModels = _committedModelCount > 0;
            }

            if (metadata.RemovedChildren != null)
            {
                _removedModelCount = metadata.RemovedChildren.Length;
                _containsCommittedModels = metadata.RemovedChildren.Length > 0;

                foreach (ModelMetadata modelMetadata in metadata.RemovedChildren)
                {
                    _internalRemovedModels.Add(factory.CreateModel<T>(modelMetadata, this));
                }
            }
        }

        #endregion

        #region Private

        private List<T> InternalList
        {
            get
            {
                return _internalList;
            }
        }

        private List<T> InternalRemovedModels
        {
            get
            {
                return _internalRemovedModels;
            }
        }

        private void Swap(T x, Int32 xIndex, T y, Int32 yIndex)
        {
            if (xIndex != yIndex)
            {
                InternalList[yIndex] = x;
                InternalList[xIndex] = y;
                RaiseItemsMoved(x, xIndex, y, yIndex);
                RaisePropertyChanged();
            }
        }

        private void AssignIsDirty()
        {
            if (this.CheckSetsIsDirty())
            {
                IsDirty = ContainsAddedModels ||
                          ContainsRemovedModels;
            }
        }

        private void CommitInternal(T model)
        {
            if (model == null)
            {
                throw new ArgumentNullException("model");
            }

            if (model.ParentProperty != this)
            {
                throw new ArgumentException("Does not belong to this collection", "model");
            }

            if (model.IsAddedToCollection)
            {
                model.IsCommittedToCollection = true;
                model.IsAddedToCollection = false;
                model.RaiseOnCommittedToCollection(this);
                CommittedModelCount++;
                AddedModelCount--;
            }
            else if (model.IsRemovedFromCollection)
            {
                InternalRemovedModels.Remove(model);
                model.IsRemovedFromCollection = false;
                model.SetParent(null, null, false, false);
                model.RaiseOnRemovedFromCollection(this);
                RemovedModelCount--;
            }
            else
            {
                throw new ArgumentException
                (
                    "Model is already committed to this collection",
                    "model"
                );
            }
        }

        private void InsertInternal
        (
            Int32 index, 
            T model, 
            Boolean undoRemoveOperationInProgress, 
            Boolean isAddingCommitted,
            Boolean raiseEventAndValidate
        )
        {
            if (!undoRemoveOperationInProgress)
            {
                if (model == null)
                {
                    throw new ArgumentNullException("model");
                }

                if (Parent == model)
                {
                    throw new ArgumentException("A model cannot be a child of itself");
                }

                if (model.ParentProperty == this)
                {
                    throw new ArgumentException("Model is already a child of this collection");
                }

                if (model.Parent != null)
                {
                    throw new ArgumentException("Model is already a child of another model property");
                }

                InsertModelToInternalList(model, index, raiseEventAndValidate);
                model.SetParent(Parent, this, true, true);
                model.IsAddedToCollection = true;
                model.RaiseOnAddedToCollection(this);

                if (!isAddingCommitted)
                {
                    AddedModelCount++;
                }
                else
                {
                    model.IsCommittedToCollection = true;
                    model.IsAddedToCollection = false;
                    model.RaiseOnCommittedToCollection(this);
                    CommittedModelCount++;
                }
            }
            else
            {
                InsertModelToInternalList(model, index, raiseEventAndValidate);
                InternalRemovedModels.Remove(model);
                model.IsCommittedToCollection = true;
                model.IsRemovedFromCollection = false;
                model.SignalRestoredToParentProperty(true, true);
                model.RaiseOnCommittedToCollection(this);
                CommittedModelCount++;
                RemovedModelCount--;
            }

            if (raiseEventAndValidate)
            {
                Validate();
            }
        }

        private void RemoveInternal(T model, Int32 index, Boolean isRemovingCommitted, Boolean raiseEventAndValidate)
        {
            if (model.IsCommittedToCollection)
            {
                if (!isRemovingCommitted)
                {
                    RemoveModelFromInternalList(model, index, raiseEventAndValidate);
                    InternalRemovedModels.Add(model);
                    model.IsRemovedFromCollection = true;
                    model.IsCommittedToCollection = false;
                    model.SignalRemovedFromParentProperty(true, true);
                    model.RaiseOnMarkedForRemovalFromCollection(this);
                    RemovedModelCount++;
                    CommittedModelCount--;
                }
                else
                {
                    RemoveModelFromInternalList(model, index, raiseEventAndValidate);
                    model.IsCommittedToCollection = false;
                    model.SetParent(null, null, true, true);
                    model.RaiseOnRemovedFromCollection(this);
                    CommittedModelCount--;
                }
            }
            else
            {
                RemoveModelFromInternalList(model, index, raiseEventAndValidate);
                model.IsAddedToCollection = false;
                model.SetParent(null, null, true, true);
                model.RaiseOnRemovedFromCollection(this);
                AddedModelCount--;
            }

            if (raiseEventAndValidate)
            {
                Validate();
            }
        }

        private void InsertModelToInternalList(T model, Int32 index, Boolean raiseEventAndValidate)
        {
            InternalList.Insert(index, model);
            RaiseNotifyPropertyChanged("Count");

            if (raiseEventAndValidate)
            {
                RaiseItemAddedOrRemoved(NotifyCollectionChangedAction.Add, model, index);
            }

            RaisePropertyChanged();
        }

        private void RemoveModelFromInternalList(T model, Int32 index, Boolean raiseEventAndValidate)
        {
            InternalList.Remove(model);
            RaiseNotifyPropertyChanged("Count");

            if (raiseEventAndValidate)
            {
                RaiseItemAddedOrRemoved(NotifyCollectionChangedAction.Remove, model, index);
            }

            RaisePropertyChanged();
        }

        private void RaiseItemAddedOrRemoved
        (
            NotifyCollectionChangedAction action,
            ModelBase model,
            Int32 index
        )
        {
            if (_collectionChanged != null)
            {
                _collectionChanged
                (
                    this,
                    new NotifyCollectionChangedEventArgs(action, model, index)
                );
            }
        }

        private void RaiseItemsMoved
        (
            T x,
            Int32 xIndex,
            T y,
            Int32 yIndex
        )
        {
            if (_collectionChanged != null)
            {
                _collectionChanged
                (
                    this,
                    new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, y, x, xIndex)
                );

                _collectionChanged
                (
                    this,
                    new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, x, y, yIndex)
                );
            }
        }

        private void RaiseReset()
        {
            if (_collectionChanged != null)
            {
                _collectionChanged
                (
                    this,
                    new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)
                );
            }
        }

        private void RaisePropertyChanged()
        {
            if (SupportsPropertyChangeTracking && Parent.PropertyChangeCounter != null)
            {
                Parent.PropertyChangeCounter.IncrementChangeCount(this);
            }
        }

        private void OnAddAsync(Object state)
        {
            Object[] stateArray = state as Object[];
            Add(stateArray[0] as T, (Boolean)stateArray[1]);
        }

        private static Boolean CheckIsAddedModel(T model)
        {
            return model.IsAddedToCollection;
        }

        private static Boolean CheckIsCommittedModel(T model)
        {
            return model.IsCommittedToCollection;
        }

        #endregion
    }
}
